﻿/* Copyright (c) Microsoft Corporation
 * 
 * All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.  You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * 
 * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
 * 
 * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
*/


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.ComponentModel;
using System.Threading;
using System.Collections.ObjectModel;
using System.Diagnostics;


namespace ProximityTapper
{
    class CrossoverConnection : INotifyPropertyChanged
    {
        private byte[] HANDSHAKE_DATA = new byte[] { 0x68, 0x2C, 0x7D, 0x9C, 0xD8, 0x5A, 0x14, 0x4A, 0xBE, 0x20, 0xF8, 0x74, 0x1D, 0x60, 0xD1, 0x00 };

        private struct ProximityDeviceConnection
        {
            public ProximityDeviceConnectInfo DeviceInfo;
            public TcpClient Client;
        }

        List<ProximityDeviceConnection> connections = new List<ProximityDeviceConnection>();

        public ObservableCollection<ProximityMessage> Messages
        {
            get;
            set;
        }

        public CrossoverConnection()
        {
            Messages = new ObservableCollection<ProximityMessage>();
            Caption = "...";
        }

        private string _caption;
        public string Caption
        {
            get { return _caption; }
            set { if (_caption != value) { _caption = value; OnPropertyChanged("Caption"); } }
        }

        private bool _failed;
        public bool Failed
        {
            get { return _failed; }
            set { if (_failed != value) { _failed = value; OnPropertyChanged("Failed"); } }
        }

        public void AddConnection(ProximityDeviceConnectInfo connectInfo)
        {
            TcpClient client = new TcpClient();

            try
            {
                client.BeginConnect(connectInfo.IpAddress, 9299, (res) =>
                {
                    try
                    {
                        client.EndConnect(res);
                        var stream = client.GetStream();
                        stream.BeginWrite(HANDSHAKE_DATA, 0, HANDSHAKE_DATA.Length, (res2) =>
                        {
                            try
                            {
                                stream.EndWrite(res2);
                                var recvdhandshake = new byte[HANDSHAKE_DATA.Length];
                                stream.BeginRead(recvdhandshake, 0, recvdhandshake.Length, (res3) =>
                                {
                                    try
                                    {
                                        int count = stream.EndRead(res3);
                                        if (count == recvdhandshake.Length
                                            && Enumerable.SequenceEqual(recvdhandshake, HANDSHAKE_DATA))
                                        {
                                            // Connected successfully
                                            lock (Messages)
                                            {
                                                // Send previous messages
                                                foreach (var m in Messages)
                                                {
                                                    Debug.WriteLine("Sending prev msg '" + m.MessageType + "' to device '" + connectInfo.Name + "'");
                                                    stream.BeginWrite(m.ToPacketBytes(), 0, ProximityMessage.MESSAGE_PACKET_SIZE, (res4) =>
                                                    {
                                                        try
                                                        {
                                                            stream.EndWrite(res4);
                                                            m.TargetDevices.Add(connectInfo);
                                                        }
                                                        catch (Exception ex)
                                                        {
                                                            this.Caption += "-Failed in AddConnection prev msg loop: " + ex.ToString();
                                                            this.Failed = true;
                                                        }
                                                    }, null);
                                                }

                                                connections.Add(new ProximityDeviceConnection() { DeviceInfo = connectInfo, Client = client });
                                            }
                                            DoBeginRead(stream, client, connectInfo);
                                            Caption += " - " + connectInfo.IpAddress;
                                        }
                                        else
                                        {
                                            // Failed
                                            this.Caption += "-Failed in AddConnection, received invalid handshake data";
                                            this.Failed = true;
                                        }
                                    }
                                    catch (Exception ex)
                                    {
                                        // Failed
                                        this.Caption += "-Failed in AddConnection #3: " + ex.ToString();
                                        this.Failed = true;
                                    }
                                }, null);
                            }
                            catch (Exception ex)
                            {
                                // Failed
                                this.Caption += "-Failed in AddConnection #4: " + ex.ToString();
                                this.Failed = true;
                            }
                        }, null);
                    }
                    catch (Exception ex)
                    {
                        // Failed
                        this.Caption += "-Failed in AddConnection #5: " + ex.ToString();
                        this.Failed = true;
                    }
                }, null);
            }
            catch (Exception ex)
            {
                // Failed
                this.Caption += "-Failed in AddConnection #6: " + ex.ToString();
                this.Failed = true;
            }
        }

        public void ClearConnections()
        {
            foreach (var c in connections)
            {
                if (c.Client.Connected)
                {
                    c.Client.Close();
                }
            }
            connections.Clear();
            Caption = "Closed";
        }

        void DoBeginRead(NetworkStream stream, TcpClient client, ProximityDeviceConnectInfo deviceInfo, byte [] packet = null, int offset = 0)
        {
            try
            {
                if (packet == null)
                {
                    packet = new byte[ProximityMessage.MESSAGE_PACKET_SIZE];
                }
                stream.BeginRead(packet, offset, packet.Length - offset, (res) =>
                {
                    try
                    {
                        Debug.WriteLine("DoBeginRead: Read data");
                        var bytesRead = stream.EndRead(res);
                        if ((bytesRead + offset) == ProximityMessage.MESSAGE_PACKET_SIZE)
                        {
                            lock (Messages)
                            {
                                var msg = new ProximityMessage(packet, deviceInfo, this);
                                SendMessageExclude(msg, client);
                            }
                        }
                        else if((bytesRead + offset) < ProximityMessage.MESSAGE_PACKET_SIZE)
                        {
                            Debug.WriteLine("DoBeginRead: Cut off read = " + bytesRead);
                            DoBeginRead(stream, client, deviceInfo, packet, offset + bytesRead);
                            return;
                        }
                        else
                        {
                            Debug.WriteLine("DoBeginRead: Invalid data size read = " + bytesRead);
                            Debug.Assert(false);
                        }
                    }
                    catch (System.ObjectDisposedException)
                    {
                        // Normal if the connection is being torn down
                    }
                    catch (Exception ex)
                    {
                        // Failed
                        this.Caption += "-Failed in DoBeginReadCB: " + ex.ToString();
                        Debug.Assert(false);
                    }
                    DoBeginRead(stream, client, deviceInfo);
                }, null);
            }
            catch (System.ObjectDisposedException)
            {
                // Normal if the connection is being torn down
            }
            catch (Exception ex)
            {
                // Failed
                this.Caption += "-Failed in DoBeginRead: " + ex.ToString();
                Debug.Assert(false);
            }           
        }

        public void AddMessage(ProximityMessage msg)
        {
            Debug.WriteLine("AddMessage: " + msg.MessageType);
            lock (Messages)
            {
                if (!Messages.Contains(msg))
                {
                    System.Windows.Application.Current.Dispatcher.Invoke(new ThreadStart(() =>
                    {
                        Messages.Add(msg);
                        Debug.WriteLine("AddMessageComplete: " + msg.MessageType);
                    }));
                }
            }
        }

        public void SendMessage(ProximityMessage msg, ProximityDeviceConnectInfo ci)
        {
            AddMessage(msg);
            var con = (from c in connections where c.DeviceInfo == ci select c).SingleOrDefault();

            SendMessage(msg, con);
        }

        void SendMessage(ProximityMessage msg, ProximityDeviceConnection c)
        {
            AddMessage(msg);

            try
            {
                var packet = msg.ToPacketBytes();
                var stream = c.Client.GetStream();
                var deviceInfo = c.DeviceInfo;

                stream.BeginWrite(packet, 0, packet.Length, (res) =>
                {
                    try
                    {
                        stream.EndWrite(res);
                        msg.TargetDevices.Add(deviceInfo);
                    }
                    catch (Exception ex)
                    {
                        this.Caption += "-Failed in SendPacketCB: " + ex.ToString();
                    }
                }, null);
            }
            catch (Exception ex)
            {
                this.Caption += "-Failed in SendPacket: " + ex.ToString();
            }
        }

        public void SendMessageExclude(ProximityMessage msg, TcpClient exclude)
        {
            AddMessage(msg);

            foreach (var c in connections)
            {
                if (c.Client != exclude && c.Client.Connected)
                {
                    SendMessage(msg, c);
                }
            }
        }

        public void SendBinaryMessage(string type, byte[] data)
        {
            SendMessageExclude(new ProximityMessage(type, data, this), null);
        }

        public void SendStringMessage(string type, string data, bool nullTerminate)
        {
            SendMessageExclude(new ProximityMessage(type, data, nullTerminate, this), null);
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string name)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                System.Windows.Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() =>
                                            {
                                                handler(this, new PropertyChangedEventArgs(name));
                                            }));
            }
        }
    }
}
